<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
</head>

<body>
    <div id="TemplateConfigPage" data-role="page" class="page type-interior pluginConfigurationPage"
        data-require="emby-input,emby-button,emby-select,emby-checkbox">
        <div data-role="content">
            <div class="content-primary">
                <form id="FingerprintConfigForm">
                    <fieldset class="verticalSection-extrabottompadding">
                        <legend>Analysis</legend>

                        <div class="checkboxContainer checkboxContainer-withDescription">
                            <label class="emby-checkbox-label">
                                <input id="AnalyzeSeasonZero" type="checkbox" is="emby-checkbox" />
                                <span>Analyze show extras</span>
                            </label>

                            <div class="fieldDescription">
                                Analyze show extras (specials).
                            </div>
                        </div>

                        <div class="inputContainer">
                            <label class="inputLabel inputLabelUnfocused" for="MaxParallelism">
                                Maximum degree of parallelism
                            </label>
                            <input id="MaxParallelism" type="number" is="emby-input" min="1" />
                            <div class="fieldDescription">
                                Maximum degree of parallelism to use when analyzing episodes.
                            </div>
                        </div>

                        <div class="inputContainer">
                            <label class="inputLabel inputLabelUnfocused" for="SelectedLibraries">
                                Limit analysis to the following libraries
                            </label>
                            <input id="SelectedLibraries" type="text" is="emby-input" />
                            <div class="fieldDescription">
                                Enter the names of libraries to analyze, separated by commas. If this field is left
                                blank, all libraries on the server containing television episodes will be analyzed.
                            </div>
                        </div>

                        <details id="edl">
                            <summary>EDL file generation</summary>

                            <div class="selectContainer">
                                <label class="selectLabel" for="EdlAction">EDL Action</label>
                                <select is="emby-select" id="EdlAction" class="emby-select-withcolor emby-select">
                                    <option value="None">
                                        None (do not create or modify EDL files)
                                    </option>

                                    <option value="CommercialBreak">
                                        Commercial Break (recommended, skips past the intro once)
                                    </option>

                                    <option value="Cut">
                                        Cut (player will remove the intro from the video)
                                    </option>

                                    <option value="Intro">
                                        Intro (show a skip button, *experimental*)
                                    </option>

                                    <option value="Mute">
                                        Mute (audio will be muted)
                                    </option>

                                    <option value="SceneMarker">
                                        Scene Marker (create a chapter marker)
                                    </option>
                                </select>

                                <div class="fieldDescription">
                                    If set to a value other than None, specifies which action to write to
                                    <a href="https://kodi.wiki/view/Edit_decision_list">MPlayer compatible EDL files</a>
                                    alongside your episode files. <br />

                                    If this value is changed after EDL files are generated, you must check the
                                    "Regenerate EDL files" checkbox below.
                                </div>
                            </div>

                            <div class="checkboxContainer checkboxContainer-withDescription">
                                <label class="emby-checkbox-label">
                                    <input id="RegenerateEdlFiles" type="checkbox" is="emby-checkbox" />
                                    <span>Regenerate EDL files during next scan</span>
                                </label>

                                <div class="fieldDescription">
                                    If checked, the plugin will <strong>overwrite all EDL files</strong> associated with
                                    your episodes with the currently discovered introduction timestamps and EDL action.
                                </div>
                            </div>
                        </details>

                        <details id="intro_reqs">
                            <summary>Modify introduction requirements</summary>

                            <div class="inputContainer">
                                <label class="inputLabel inputLabelUnfocused" for="AnalysisPercent">
                                    Percent of audio to analyze
                                </label>
                                <input id="AnalysisPercent" type="number" is="emby-input" min="1" max="90" />
                                <div class="fieldDescription">
                                    Analysis will be limited to this percentage of each episode's audio. For example, a
                                    value of 25 (the default) will limit analysis to the first quarter of each episode.
                                </div>
                            </div>

                            <div class="inputContainer">
                                <label class="inputLabel inputLabelUnfocused" for="AnalysisLengthLimit">
                                    Maximum runtime of audio to analyze (in minutes)
                                </label>
                                <input id="AnalysisLengthLimit" type="number" is="emby-input" min="1" />
                                <div class="fieldDescription">
                                    Analysis will be limited to this amount of each episode's audio. For example, a
                                    value of 10 (the default) will limit analysis to the first 10 minutes of each
                                    episode.
                                </div>
                            </div>

                            <div class="inputContainer">
                                <label class="inputLabel inputLabelUnfocused" for="MinimumIntroDuration">
                                    Minimum introduction duration (in seconds)
                                </label>
                                <input id="MinimumIntroDuration" type="number" is="emby-input" min="1" />
                                <div class="fieldDescription">
                                    Similar sounding audio which is shorter than this duration will not be considered an
                                    introduction.
                                </div>
                            </div>

                            <div class="inputContainer">
                                <label class="inputLabel inputLabelUnfocused" for="MaximumIntroDuration">
                                    Maximum introduction duration (in seconds)
                                </label>
                                <input id="MaximumIntroDuration" type="number" is="emby-input" min="1" />
                                <div class="fieldDescription">
                                    Similar sounding audio which is longer than this duration will not be considered an
                                    introduction.
                                </div>
                            </div>

                            <p>
                                The amount of each episode's audio that will be analyzed is determined using both
                                the percentage of audio and maximum runtime of audio to analyze. The minimum of
                                (episode duration * percent, maximum runtime) is the amount of audio that will
                                be analyzed.
                            </p>

                            <p>
                                If the audio percentage or maximum runtime settings are modified, the cached
                                fingerprints and introduction timestamps for each season you want to analyze with the
                                modified settings <strong>will have to be deleted.</strong>

                                Increasing either of these settings will cause episode analysis to take much longer.
                            </p>
                        </details>

                        <details id="silence">
                            <summary>Silence detection options</summary>

                            <div class="inputContainer">
                                <label class="inputLabel inputLabelUnfocused" for="SilenceDetectionMaximumNoise">
                                    Noise tolerance
                                </label>
                                <input id="SilenceDetectionMaximumNoise" type="number" is="emby-input" min="-90"
                                    max="0" />
                                <div class="fieldDescription">
                                    Noise tolerance in negative decibels.
                                </div>
                            </div>

                            <div class="inputContainer">
                                <label class="inputLabel inputLabelUnfocused" for="SilenceDetectionMinimumDuration">
                                    Minimum silence duration
                                </label>
                                <input id="SilenceDetectionMinimumDuration" type="number" is="emby-input" min="0"
                                    step="0.01" />
                                <div class="fieldDescription">
                                    Minimum silence duration in seconds before adjusting introduction end time.
                                </div>
                            </div>
                        </details>
                    </fieldset>

                    <fieldset class="verticalSection-extrabottompadding">
                        <legend>Playback</legend>

                        <div class="checkboxContainer checkboxContainer-withDescription">
                            <label class="emby-checkbox-label">
                                <input id="SkipButtonVisible" type="checkbox" is="emby-checkbox" />
                                <span>Show skip intro button</span>
                            </label>

                            <div class="fieldDescription">
                                If checked, a skip button will be displayed at the start of an episode's introduction.
                                <strong>This setting only applies to the web interface.</strong>
                                <br />
                            </div>
                        </div>

                        <div class="checkboxContainer checkboxContainer-withDescription">
                            <label class="emby-checkbox-label">
                                <input id="AutoSkip" type="checkbox" is="emby-checkbox" />
                                <span>Automatically skip intros</span>
                            </label>

                            <div class="fieldDescription">
                                If checked, intros will be automatically skipped. If you access Jellyfin through a
                                reverse proxy, it must be configured to proxy web
                                sockets.<br />
                            </div>
                        </div>

                        <div class="checkboxContainer checkboxContainer-withDescription">
                            <label class="emby-checkbox-label">
                                <input id="SkipFirstEpisode" type="checkbox" is="emby-checkbox" />
                                <span>Automatically skip intros in the first episode of a season</span>
                            </label>

                            <div class="fieldDescription">
                                If checked, auto skip will skip introductions in the first episode of a season.<br />
                            </div>
                        </div>

                        <div class="inputContainer">
                            <label class="inputLabel inputLabelUnfocused" for="ShowPromptAdjustment">
                                Show skip prompt at
                            </label>
                            <input id="ShowPromptAdjustment" type="number" is="emby-input" min="0" />
                            <div class="fieldDescription">
                                Seconds before the introduction starts to display the skip prompt at.
                            </div>
                        </div>

                        <div class="inputContainer">
                            <label class="inputLabel inputLabelUnfocused" for="HidePromptAdjustment">
                                Hide skip prompt after
                            </label>
                            <input id="HidePromptAdjustment" type="number" is="emby-input" min="2" />
                            <div class="fieldDescription">
                                Seconds after the introduction starts to hide the skip prompt at.
                            </div>
                        </div>

                        <div class="inputContainer">
                            <label class="inputLabel inputLabelUnfocused" for="SecondsOfIntroToPlay">
                                Seconds of intro to play
                            </label>
                            <input id="SecondsOfIntroToPlay" type="number" is="emby-input" min="0" />
                            <div class="fieldDescription">
                                Seconds of introduction that should be played. Defaults to 2.
                            </div>
                        </div>

                        <details>
                            <summary>User Interface Customization</summary>

                            <div class="inputContainer">
                                <label class="inputLabel" for="SkipButtonIntroText">
                                    Skip intro button text
                                </label>
                                <input id="SkipButtonIntroText" type="text" is="emby-input" />
                                <div class="fieldDescription">
                                    Text to display in the skip intro button.
                                </div>
                            </div>

                            <div class="inputContainer">
                                <label class="inputLabel" for="SkipButtonEndCreditsText">
                                    Skip end credits button text
                                </label>
                                <input id="SkipButtonEndCreditsText" type="text" is="emby-input" />
                                <div class="fieldDescription">
                                    Text to display in the skip end credits button.
                                </div>
                            </div>

                            <div class="inputContainer">
                                <label class="inputLabel" for="AutoSkipNotificationText">
                                    Automatic skip notification message
                                </label>
                                <input id="AutoSkipNotificationText" type="text" is="emby-input" />
                                <div class="fieldDescription">
                                    Message sent to a user after automatically skipping an introduction. Leave blank to skip sending a notification.
                                </div>
                            </div>
                        </details>
                    </fieldset>

                    <div>
                        <button is="emby-button" type="submit" class="raised button-submit block emby-button">
                            <span>Save</span>
                        </button>
                        <br />
                    </div>
                </form>
            </div>

            <details id="support">
                <summary>Support Bundle</summary>

                <textarea id="supportBundle" rows="20" cols="75" readonly></textarea>
            </details>

            <details id="visualizer">
                <summary>Advanced</summary>

                <h3 style="margin:0">Select episodes to manage</h3>
                <select id="troubleshooterShow"></select>
                <select id="troubleshooterSeason"></select>
                <br />

                <select id="troubleshooterEpisode1"></select>
                <select id="troubleshooterEpisode2"></select>
                <br />
                <br />

                <div id="timestampEditor" style="display:none">
                    <h3 style="margin:0">Introduction timestamp editor</h3>
                    <p style="margin:0">All times are in seconds.</p>

                    <p id="editLeftEpisodeTitle" style="margin-bottom:0"></p>
                    <input style="width:4em" type="number" min="0" id="editLeftEpisodeStart"> to
                    <input style="width:4em;margin-bottom:10px" type="number" min="0" id="editLeftEpisodeEnd">

                    <p id="editRightEpisodeTitle" style="margin-top:0;margin-bottom:0"></p>
                    <input style="width:4em" type="number" min="0" id="editRightEpisodeStart"> to
                    <input style="width:4em;margin-bottom:10px" type="number" min="0" id="editRightEpisodeEnd">
                    <br />

                    <button id="btnUpdateTimestamps" type="button">
                        Update timestamps
                    </button>
                    <br />
                    <br />

                    <button id="btnEraseSeasonTimestamps" type="button">
                        Erase all timestamps for this season
                    </button>
                    <hr />
                </div>

                <button id="btnEraseIntroTimestamps">
                    Erase all introduction timestamps (globally)
                </button>
                <br />

                <button id="btnEraseCreditTimestamps">
                    Erase all end credits timestamps (globally)
                </button>
                <br />
                <br />

                <h3>Fingerprint Visualizer</h3>
                <p>
                    Interactively compare the audio fingerprints of two episodes. <br />
                    The blue and red bar to the right of the fingerprint diff turns blue
                    when the corresponding fingerprint points are at least 80% similar.
                </p>
                <table>
                    <thead>
                        <td style="min-width: 100px; font-weight: bold">Key</td>
                        <td style="font-weight: bold">Function</td>
                    </thead>
                    <tbody>
                        <tr>
                            <td>Up arrow</td>
                            <td>
                                Shift the left episode up by 0.128 seconds.
                                Holding control will shift the episode by 10 seconds.
                            </td>
                        </tr>
                        <tr>
                            <td>Down arrow</td>
                            <td>
                                Shift the left episode down by 0.128 seconds.
                                Holding control will shift the episode by 10 seconds.
                            </td>
                        </tr>
                        <tr>
                            <td>Right arrow</td>
                            <td>Advance to the next pair of episodes.</td>
                        </tr>
                        <tr>
                            <td>Left arrow</td>
                            <td>Go back to the previous pair of episodes.</td>
                        </tr>
                    </tbody>
                </table>
                <br />

                <span>Shift amount:</span>
                <input type="number" min="-3000" max="3000" value="0" id="offset">
                <br />
                <span id="suggestedShifts">Suggested shifts:</span>
                <br />
                <br />

                <canvas id="troubleshooter"></canvas>
                <span id="timestampContainer">
                    <span id="timestamps"></span> <br />
                    <span id="intros"></span>
                </span>
            </details>
        </div>

        <script src="configurationpage?name=visualizer.js"></script>

        <script>
            // first and second episodes to fingerprint & compare
            var lhs = [];
            var rhs = [];

            // fingerprint point comparison & miminum similarity threshold (at most 6 bits out of 32 can be different)
            var fprDiffs = [];
            var fprDiffMinimum = (1 - 6 / 32) * 100;

            // seasons grouped by show
            var shows = {};

            // settings elements
            var visualizer = document.querySelector("details#visualizer");
            var support = document.querySelector("details#support");
            var btnEraseIntroTimestamps = document.querySelector("button#btnEraseIntroTimestamps");
            var btnEraseCreditTimestamps = document.querySelector("button#btnEraseCreditTimestamps");

            // all plugin configuration fields that can be get or set with .value (i.e. strings or numbers).
            var configurationFields = [
                // analysis
                "MaxParallelism",
                "SelectedLibraries",
                "AnalysisPercent",
                "AnalysisLengthLimit",
                "MinimumIntroDuration",
                "MaximumIntroDuration",
                "EdlAction",
                // playback
                "ShowPromptAdjustment",
                "HidePromptAdjustment",
                "SecondsOfIntroToPlay",
                // internals
                "SilenceDetectionMaximumNoise",
                "SilenceDetectionMinimumDuration",
                // UI customization
                "SkipButtonIntroText",
                "SkipButtonEndCreditsText",
                "AutoSkipNotificationText"
            ]

            var booleanConfigurationFields = [
                "AnalyzeSeasonZero",
                "RegenerateEdlFiles",
                "AutoSkip",
                "SkipFirstEpisode",
                "SkipButtonVisible",
            ]

            // visualizer elements
            var canvas = document.querySelector("canvas#troubleshooter");
            var selectShow = document.querySelector("select#troubleshooterShow");
            var selectSeason = document.querySelector("select#troubleshooterSeason");
            var selectEpisode1 = document.querySelector("select#troubleshooterEpisode1");
            var selectEpisode2 = document.querySelector("select#troubleshooterEpisode2");
            var txtOffset = document.querySelector("input#offset");
            var txtSuggested = document.querySelector("span#suggestedShifts");
            var btnSeasonEraseTimestamps = document.querySelector("button#btnEraseSeasonTimestamps");
            var btnUpdateTimestamps = document.querySelector("button#btnUpdateTimestamps");
            var timeContainer = document.querySelector("span#timestampContainer");

            var windowHashInterval = 0;

            // when the fingerprint visualizer opens, populate show names
            async function visualizerToggled() {
                if (!visualizer.open) {
                    return;
                }

                // ensure the series select is empty
                while (selectShow.options.length > 0) {
                    selectShow.remove(0);
                }

                Dashboard.showLoadingMsg();

                shows = await getJson("Intros/Shows");

                var sorted = [];
                for (var series in shows) { sorted.push(series); }
                sorted.sort();

                for (var show of sorted) {
                    addItem(selectShow, show, show);
                }

                selectShow.value = "";

                Dashboard.hideLoadingMsg();
            }

            // fetch the support bundle whenever the detail section is opened.
            async function supportToggled() {
                if (!support.open) {
                    return;
                }

                // Fetch the support bundle
                const bundle = await fetchWithAuth("IntroSkipper/SupportBundle", "GET", null);
                const bundleText = await bundle.text();

                // Display it to the user and select all
                const ta = document.querySelector("textarea#supportBundle");
                ta.value = bundleText;
                ta.focus();
                ta.setSelectionRange(0, ta.value.length);

                // Attempt to copy it to the clipboard automatically, falling back
                // to prompting the user to press Ctrl + C.
                try {
                    navigator.clipboard.writeText(bundleText);
                    Dashboard.alert("Support bundle copied to clipboard");
                } catch {
                    Dashboard.alert("Press Ctrl+C to copy support bundle");
                }
            }

            // show changed, populate seasons
            async function showChanged() {
                clearSelect(selectSeason);

                // add all seasons from this show to the season select
                for (var season of shows[selectShow.value]) {
                    addItem(selectSeason, season, season);
                }

                selectSeason.value = "";
            }

            // season changed, reload all episodes
            async function seasonChanged() {
                const url = "Intros/Show/" + encodeURI(selectShow.value) + "/" + selectSeason.value;
                const episodes = await getJson(url);

                clearSelect(selectEpisode1);
                clearSelect(selectEpisode2);

                let i = 1;
                for (let episode of episodes) {
                    const strI = i.toLocaleString("en", { minimumIntegerDigits: 2, maximumFractionDigits: 0 });
                    addItem(selectEpisode1, strI + ": " + episode.Name, episode.Id);
                    addItem(selectEpisode2, strI + ": " + episode.Name, episode.Id);
                    i++;
                }

                setTimeout(() => {
                    selectEpisode1.selectedIndex = 0;
                    selectEpisode2.selectedIndex = 1;
                    episodeChanged();
                }, 100);
            }

            // episode changed, get fingerprints & calculate diff
            async function episodeChanged() {
                if (!selectEpisode1.value || !selectEpisode2.value) {
                    return;
                }

                Dashboard.showLoadingMsg();

                lhs = await getJson("Intros/Episode/" + selectEpisode1.value + "/Chromaprint");
                rhs = await getJson("Intros/Episode/" + selectEpisode2.value + "/Chromaprint");

                Dashboard.hideLoadingMsg();

                txtOffset.value = "0";
                refreshBounds();
                renderTroubleshooter();
                findExactMatches();
                updateTimestampEditor();
            }

            // updates the timestamp editor
            async function updateTimestampEditor() {
                // Get the title and ID of the left and right episodes
                const leftEpisode = selectEpisode1.options[selectEpisode1.selectedIndex];
                const rightEpisode = selectEpisode2.options[selectEpisode2.selectedIndex];

                // Try to get the timestamps of each intro, falling back a default value of zero if no intro was found
                let leftEpisodeIntro = await getJson("Episode/" + leftEpisode.value + "/IntroTimestamps/v1");
                if (!leftEpisodeIntro.hasOwnProperty("IntroStart")) {
                    leftEpisodeIntro = { IntroStart: 0, IntroEnd: 0 };
                }

                let rightEpisodeIntro = await getJson("Episode/" + rightEpisode.value + "/IntroTimestamps/v1");
                if (!rightEpisodeIntro.hasOwnProperty("IntroStart")) {
                    rightEpisodeIntro = { IntroStart: 0, IntroEnd: 0 };
                }

                // Update the editor for the first and second episodes
                document.querySelector("#timestampEditor").style.display = "unset";
                document.querySelector("#editLeftEpisodeTitle").textContent = leftEpisode.text;
                document.querySelector("#editLeftEpisodeStart").value = Math.round(leftEpisodeIntro.IntroStart);
                document.querySelector("#editLeftEpisodeEnd").value = Math.round(leftEpisodeIntro.IntroEnd);

                document.querySelector("#editRightEpisodeTitle").textContent = rightEpisode.text;
                document.querySelector("#editRightEpisodeStart").value = Math.round(rightEpisodeIntro.IntroStart);
                document.querySelector("#editRightEpisodeEnd").value = Math.round(rightEpisodeIntro.IntroEnd);
            }

            // adds an item to a dropdown
            function addItem(select, text, value) {
                let item = new Option(text, value);
                select.add(item);
            }

            // clear a select of items
            function clearSelect(select) {
                let i, L = select.options.length - 1;
                for (i = L; i >= 0; i--) {
                    select.remove(i);
                }
            }

            // make an authenticated GET to the server and parse the response as JSON
            async function getJson(url) {
                return await fetchWithAuth(url, "GET").then(r => { return r.json(); });
            }

            // make an authenticated fetch to the server
            async function fetchWithAuth(url, method, body) {
                url = ApiClient.serverAddress() + "/" + url;

                const reqInit = {
                    method: method,
                    headers: {
                        "Authorization": "MediaBrowser Token=" + ApiClient.accessToken()
                    },
                    body: body,
                };

                if (method === "POST") {
                    reqInit.headers["Content-Type"] = "application/json";
                }

                return await fetch(url, reqInit);
            }

            // key pressed
            function keyDown(e) {
                let episodeDelta = 0;
                let offsetDelta = 0;

                switch (e.key) {
                    case "ArrowDown":
                        // if the control key is pressed, shift LHS by 10s. Otherwise, shift by 1.
                        offsetDelta = e.ctrlKey ? 10 / 0.128 : 1;
                        break;

                    case "ArrowUp":
                        offsetDelta = e.ctrlKey ? -10 / 0.128 : -1;
                        break;

                    case "ArrowRight":
                        episodeDelta = 2;
                        break;

                    case "ArrowLeft":
                        episodeDelta = -2;
                        break;

                    default:
                        return;
                }

                if (offsetDelta != 0) {
                    txtOffset.value = Number(txtOffset.value) + Math.floor(offsetDelta);
                }

                if (episodeDelta != 0) {
                    // calculate the number of episodes remaining in the LHS and RHS episode pickers
                    const lhsRemaining = selectEpisode1.selectedIndex;
                    const rhsRemaining = selectEpisode2.length - selectEpisode2.selectedIndex - 1;

                    // if we're moving forward and the right episode picker is close to the end, don't move.
                    if (episodeDelta > 0 && rhsRemaining <= 1) {
                        return;
                    } else if (episodeDelta < 0 && lhsRemaining <= 1) {
                        return;
                    }

                    selectEpisode1.selectedIndex += episodeDelta;
                    selectEpisode2.selectedIndex += episodeDelta;
                    episodeChanged();
                }

                renderTroubleshooter();
                e.preventDefault();
            }

            // check that the user is still on the configuration page
            function checkWindowHash() {
                const h = location.hash;
                if (h === "#!/configurationpage?name=Intro%20Skipper" || h.includes("#!/dialog")) {
                    return;
                }

                console.debug("navigated away from intro skipper configuration page");
                document.removeEventListener("keydown", keyDown);
                clearInterval(windowHashInterval);
            }

            // converts seconds to a readable timestamp (i.e. 127 becomes "02:07").
            function secondsToString(seconds) {
                return new Date(seconds * 1000).toISOString().substr(14, 5);
            }

            // erase all intro/credits timestamps
            function eraseTimestamps(mode) {
                const lower = mode.toLocaleLowerCase();
                const title = "Confirm timestamp erasure";
                const body = "Are you sure you want to erase all previously discovered " +
                    mode.toLocaleLowerCase() +
                    " timestamps?";

                Dashboard.confirm(
                    body,
                    title,
                    (result) => {
                        if (!result) {
                            return;
                        }

                        fetchWithAuth("Intros/EraseTimestamps?mode=" + mode, "POST", null);

                        Dashboard.alert(mode + " timestamps erased");
                    });
            }

            document.querySelector('#TemplateConfigPage')
                .addEventListener('pageshow', function () {
                    Dashboard.showLoadingMsg();
                    ApiClient.getPluginConfiguration("c83d86bb-a1e0-4c35-a113-e2101cf4ee6b").then(function (config) {
                        for (const field of configurationFields) {
                            document.querySelector("#" + field).value = config[field];
                        }

                        for (const field of booleanConfigurationFields) {
                            document.querySelector("#" + field).checked = config[field];
                        }

                        Dashboard.hideLoadingMsg();
                    });
                });

            document.querySelector('#FingerprintConfigForm')
                .addEventListener('submit', function (e) {
                    Dashboard.showLoadingMsg();
                    ApiClient.getPluginConfiguration("c83d86bb-a1e0-4c35-a113-e2101cf4ee6b").then(function (config) {
                        for (const field of configurationFields) {
                            config[field] = document.querySelector("#" + field).value;
                        }

                        for (const field of booleanConfigurationFields) {
                            config[field] = document.querySelector("#" + field).checked;
                        }

                        ApiClient.updatePluginConfiguration("c83d86bb-a1e0-4c35-a113-e2101cf4ee6b", config)
                            .then(function (result) {
                                Dashboard.processPluginConfigurationUpdateResult(result);
                            });
                    });

                    e.preventDefault();
                    return false;
                });

            visualizer.addEventListener("toggle", visualizerToggled);
            support.addEventListener("toggle", supportToggled);
            txtOffset.addEventListener("change", renderTroubleshooter);
            selectShow.addEventListener("change", showChanged);
            selectSeason.addEventListener("change", seasonChanged);
            selectEpisode1.addEventListener("change", episodeChanged);
            selectEpisode2.addEventListener("change", episodeChanged);
            btnEraseIntroTimestamps.addEventListener("click", (e) => {
                eraseTimestamps("Introduction");
                e.preventDefault();
            });
            btnEraseCreditTimestamps.addEventListener("click", (e) => {
                eraseTimestamps("Credits");
                e.preventDefault();
            });
            btnSeasonEraseTimestamps.addEventListener("click", () => {
                Dashboard.confirm(
                    "Are you sure you want to erase all timestamps for this season?",
                    "Confirm timestamp erasure",
                    (result) => {
                        if (!result) {
                            return;
                        }

                        const show = selectShow.value;
                        const season = selectSeason.value;

                        const url = "Intros/Show/" + encodeURIComponent(show) + "/" + encodeURIComponent(season);
                        fetchWithAuth(url, "DELETE", null);

                        Dashboard.alert("Erased timestamps for " + season + " of " + show);
                    }
                );
            });
            btnUpdateTimestamps.addEventListener("click", () => {
                const lhsId = selectEpisode1.options[selectEpisode1.selectedIndex].value;
                const newLhsIntro = {
                    IntroStart: document.querySelector("#editLeftEpisodeStart").value,
                    IntroEnd: document.querySelector("#editLeftEpisodeEnd").value,
                };

                const rhsId = selectEpisode2.options[selectEpisode2.selectedIndex].value;
                const newRhsIntro = {
                    IntroStart: document.querySelector("#editRightEpisodeStart").value,
                    IntroEnd: document.querySelector("#editRightEpisodeEnd").value,
                };

                fetchWithAuth("Intros/Episode/" + lhsId + "/UpdateIntroTimestamps", "POST", JSON.stringify(newLhsIntro));
                fetchWithAuth("Intros/Episode/" + rhsId + "/UpdateIntroTimestamps", "POST", JSON.stringify(newRhsIntro));

                Dashboard.alert("New introduction timestamps saved");
            });
            document.addEventListener("keydown", keyDown);
            windowHashInterval = setInterval(checkWindowHash, 2500);

            canvas.addEventListener("mousemove", (e) => {
                const rect = e.currentTarget.getBoundingClientRect();
                const y = e.clientY - rect.top;
                const shift = Number(txtOffset.value);

                let lTime, rTime, diffPos;
                if (shift < 0) {
                    lTime = y * 0.128;
                    rTime = (y + shift) * 0.128;
                    diffPos = y + shift;
                } else {
                    lTime = (y - shift) * 0.128;
                    rTime = y * 0.128;
                    diffPos = y - shift;
                }

                const diff = fprDiffs[Math.floor(diffPos)];

                if (!diff) {
                    timeContainer.style.display = "none";
                    return;
                } else {
                    timeContainer.style.display = "unset";
                }

                const times = document.querySelector("span#timestamps");

                // LHS timestamp, RHS timestamp, percent similarity
                times.textContent =
                    secondsToString(lTime) + ", " +
                    secondsToString(rTime) + ", " +
                    Math.round(diff) + "%";

                timeContainer.style.position = "relative";
                timeContainer.style.left = "25px";
                timeContainer.style.top = (-1 * rect.height + y).toString() + "px";
            });
        </script>
    </div>
</body>

</html>
